﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.Objects.DataClasses;
using System.Data.Objects;
using System.Linq.Expressions;
using System.Reflection;
using System.Data;
using System.Diagnostics.Contracts;

namespace BaseLib.Data
{
    public abstract class BaseEntityRepository<TEntity, TContext> : BaseDataRepository<TContext>
        where TContext : ObjectContext,new()
        where TEntity : EntityObject
    {
        private string _PKName = null;
        private string PKName
        {
            get
            {
                if (string.IsNullOrWhiteSpace(_PKName))
                    _PKName = GetPrimaryKeyName(typeof(TEntity));

                return _PKName;
            }
        }

        private string _entityName = null;
        protected string EntityName
        {
            get
            {
                if (string.IsNullOrWhiteSpace(_entityName))
                {
                    var entitySet = DBContext.CreateObjectSet<TEntity>().EntitySet;

                    _entityName = string.Format("{0}.{1}", entitySet.EntityContainer, entitySet.Name);
                }
                return _entityName;
            }
        }

        public BaseEntityRepository()
        {
        }
        public BaseEntityRepository(TContext context)
            : base(context)
        {

        }

        public virtual IQueryable<TEntity> GetEntitySet(params string[] includes)
        {
            ObjectQuery<TEntity> entitySet = DBContext.CreateObjectSet<TEntity>();
            foreach (string inc in includes)
            {
                entitySet = entitySet.Include(inc);
            }

            return entitySet;
        }

        public TEntity GetEntity(int id, params string[] includes)
        {
            return GetEntitySet().SingleOrDefault(GetEntityQuery(id));
        }

        public virtual TEntity Save(TEntity entity, bool saveChanges = true)
        {
            TEntity ent;
            if (entity.GetPrimaryKeyValue<int>() == 0)
                ent = Create(entity, saveChanges);
            else
                ent = Update(entity, saveChanges);

            return ent;
        }

        public virtual TEntity Create(TEntity entity, bool saveChanges = true)
        {
            DBContext.CreateObjectSet<TEntity>().AddObject(entity);

            if (saveChanges)
                SaveChanges();

            return entity;
        }

        public virtual TEntity Update(TEntity entity, bool saveChanges = true)
        {
            if (entity.EntityState != EntityState.Modified)
            {
                var primaryKeyName = GetPrimaryKeyName(typeof(TEntity));
                var primaryKeyValue = entity.GetPrimaryKeyValue<int>();
                var StubEntity = GetEntity(primaryKeyValue);
                if (StubEntity.EntityState == EntityState.Detached)
                {
                    var entityInContext = DBContext.GetObjectByKey(StubEntity.EntityKey);
                    if (entityInContext == null)
                        DBContext.AttachTo(EntityName, entity);
                    else
                        entity = (TEntity)entityInContext;
                }
            }

            DBContext.ApplyCurrentValues(EntityName, entity);
            if (saveChanges)
                SaveChanges();

            return entity;
        }

        public int SaveChanges()
        {
            try
            {
                return DBContext.SaveChanges(SaveOptions.AcceptAllChangesAfterSave);
            }
            catch
            {
#if (DEBUG)
                throw;
#endif
                return 0;
            }
        }

        public virtual int Delete(TEntity Entity, bool saveChanges = true)
        {
            var fqen = this.EntityName;
            if (Entity.EntityState == EntityState.Detached)
            {
                var entityInContext = DBContext.GetObjectByKey(Entity.EntityKey);
                if (entityInContext == null)
                    DBContext.AttachTo(fqen, Entity);
                else
                    Entity = (TEntity)entityInContext;
            }

            DBContext.DeleteObject(Entity);

            if (saveChanges)
                return SaveChanges();
            else
                return 0;
        }

        /// <summary>
        /// A generic method for deleting an entity
        /// </summary>
        /// <param name="primaryKeyValue">The id of the entity you want to delete</param>
        /// <returns>How many entities were affected</returns>
        public int Delete(int primaryKeyValue, bool saveChanges = true)
        {
            var StubEntity = GenerateStubEntity(primaryKeyValue);

            return Delete(StubEntity, saveChanges);
        }

        public virtual int Delete(IEnumerable<int> PrimaryKeyValues)
        {
            int deleteCount = 0;
            if (PrimaryKeyValues != null && PrimaryKeyValues.Any())
            {
                deleteCount =
                    this.DBContext.ExecuteStoreCommand(
                        string.Format(
                            "DELETE FROM {0} WHERE {1} IN ({2})",
                            GetTableName(),
                            PKName,
                            string.Join(",", PrimaryKeyValues)));
            }
            return deleteCount;
        }

        private Expression<Func<TEntity, bool>> GetEntityQuery(int id)
        {
            ParameterExpression e = Expression.Parameter(typeof(TEntity), "e");
            PropertyInfo propinfo = typeof(TEntity).GetProperty(GetPrimaryKeyName(typeof(TEntity)));
            MemberExpression m = Expression.MakeMemberAccess(e, propinfo);
            ConstantExpression c = Expression.Constant(id, typeof(int));
            BinaryExpression b = Expression.Equal(m, c);
            Expression<Func<TEntity, bool>> lambda = Expression.Lambda<Func<TEntity, bool>>(b, e);
            return lambda;
        }

        private static string GetPrimaryKeyName(Type type)
        {
            string tempPrimaryKeyName = "";
            PropertyInfo[] props = type.GetProperties();
            foreach (PropertyInfo prop in props)
            {

                foreach (var attr in prop.GetCustomAttributes(typeof(EdmScalarPropertyAttribute), false))
                {
                    EdmScalarPropertyAttribute edmProp = (EdmScalarPropertyAttribute)attr;

                    if (edmProp.EntityKeyProperty)
                    {
                        tempPrimaryKeyName = prop.Name;
                        break;
                    }
                    if (!String.IsNullOrEmpty(tempPrimaryKeyName))

                        break;
                }
                if (!String.IsNullOrEmpty(tempPrimaryKeyName))

                    break;
            }
            return tempPrimaryKeyName;
            //return typeof(TEntity).GetProperties().Where(property => ((EdmScalarPropertyAttribute)property.GetCustomAttributes(typeof(EdmScalarPropertyAttribute), false).FirstOrDefault()).EntityKeyProperty).FirstOrDefault().Name;        
        }

        protected TEntity GenerateStubEntity(int PrimaryKeyValue)
        {
            TEntity StubEntity = DBContext.CreateObject<TEntity>();
            StubEntity.EntityKey = new EntityKey(EntityName, PKName, PrimaryKeyValue);

            typeof(TEntity).GetProperty(PKName).SetValue(StubEntity, PrimaryKeyValue, null);

            return StubEntity;
        }

        private static string GetTableName()
        {
            Type type = typeof(TEntity);
            var at = GetAttribute<EdmEntityTypeAttribute>(type);
            return at.Name;
        }

        private static T GetAttribute<T>(MemberInfo memberInfo) where T : class
        {
            Contract.Requires(memberInfo != null, "memberInfo is null.");
            Contract.Ensures(Contract.Result<T>() != null);

            object[] customAttributes = memberInfo.GetCustomAttributes(typeof(T), false);
            T attribute = customAttributes.Where(a => a is T).First() as T;
            return attribute;
        }
    }
}
